<?php

namespace App\Util\WechatPay;

use App\Exceptions\ServiceException;
use App\Exceptions\ServiceExceptionConstant;
use App\Util\Helper;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
use WeChatPay\Builder;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;

/**
 *
 * Author: TheSmile
 * Date: 2023/4/28 13:52
 * Email: <594652523@qq.com>
 */
class WechatPayUtil
{
    //1.创建加载商户私钥、加载平台证书、初始化httpClient的通用方法
    //2.用户下单发起支付，商户可通过JSAPI下单创建支付订单
    //3.商户小程序内使用小程序调起支付API（wx.requestPayment）发起微信支付
    //4.用户支付成功后，商户可接收到微信支付支付结果通知支付通知API(https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml)
    //5.商户在没有接收到微信支付结果通知的情况下需要主动调用查询订单API(https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_2.shtml)查询支付结果。
    private $paymentConfig;

    public function __construct()
    {
        $this->paymentConfig = config('wechat.payment.default');
    }

    /**
     * 小程序支付
     * @param $param
     * @return array
     * @throws ServiceException
     */
    public function miniWechatPay($param)
    {
        $instance  = $this->initHttpClient();
        $prepayRel = $this->unifiedOrder($instance, $param);
        $nonceStr  = Helper::random(6, 1);
        $timeStamp = time();
        return $this->makeSign($nonceStr, $timeStamp, $prepayRel['prepay_id']);
    }

    /**
     * native支付(生成支付二维码）
     * @param $param
     * @return void
     */
    public function nativeWechatPay($param)
    {
        $instance  = $this->initHttpClient();
        $nonceStr  = Helper::random(6, 1);
        $timeStamp = time();
        $signStr   = $this->makeSign($nonceStr, $timeStamp);
        return $this->nativeOrder($instance, $param, $signStr);
    }

    //初始化client
    public function initHttpClient()
    {
        // 商户号
        $merchantId = $this->paymentConfig['mch_id'];
        // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyFilePath = 'file://' . dirname(__FILE__) . '/cert/apiclient_key.pem';
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
        // 「商户API证书」的「证书序列号」
        $merchantCertificateSerial = $this->paymentConfig['serial_no'];

        // 从本地文件中加载「微信支付平台证书」，用来验证微信支付应答的签名
        $platformCertificateFilePath = 'file://' . dirname(__FILE__) . '/cert/cert.pem';
        $platformPublicKeyInstance   = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

        // 从「微信支付平台证书」中获取「证书序列号」
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);

        // 构造一个 APIv3 客户端实例
        return Builder::factory([
            'mchid'      => $merchantId,
            'serial'     => $merchantCertificateSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs'      => [
                $platformCertificateSerial => $platformPublicKeyInstance,
            ],
        ]);
    }

    /**
     * jsapi统一下单
     * @param $instance
     * @param $payParam
     * @return mixed
     * @throws ServiceException
     */
    public function unifiedOrder($instance, $payParam)
    {
        try {
            $resp       = $instance
                ->chain('v3/pay/transactions/jsapi')
                ->post([
                    'json'    => [
                        "amount"       => [
//                            "total"    => $payParam['total_price'],
                            "total"    => $this->paymentConfig['pay_test'] ? 1 : $payParam['total_price'],
                            "currency" => "CNY",
                        ],
                        "mchid"        => $this->paymentConfig['mch_id'],
                        "description"  => $payParam['description'],
                        "notify_url"   => $this->paymentConfig['notify_url'],
                        "payer"        => [
                            "openid" => $payParam['openid'],
                        ],
                        "out_trade_no" => $payParam['trade_sn'],
                        "appid"        => $this->paymentConfig['app_id'],
//                    "attach" => "自定义数据说明",// 附加数据，在查询API和支付通知中原样返回，可作为自定义参数使用，实际情况下只有支付完成状态才会返回该字段

                    ],
                    'headers' => ['Accept' => 'application/json']
                ]);
            $statusCode = $resp->getStatusCode();
            if ($statusCode != 200) {
                throw new ServiceException(ServiceExceptionConstant::getErrorData('wechat_unify_order_failed'));
            }
            return json_decode($resp->getBody()->getContents(), true);
        } catch (\Exception $e) {
            Log::error('下单失败：' . $e->getMessage());
            throw new ServiceException(ServiceExceptionConstant::getErrorData('下单失败'));
        }
    }


    public function nativeOrder($instance, $payParam, $signStr)
    {
        try {
            $resp       = $instance
                ->chain('v3/pay/transactions/native')
                ->post([
                    'json'    => [
                        "amount"       => [
                            "total"    => $this->paymentConfig['pay_test'] ? 1 : $payParam['total_price'],
                            "currency" => "CNY",
                        ],
                        "mchid"        => $this->paymentConfig['mch_id'],
                        "description"  => $payParam['description'],
                        "notify_url"   => $this->paymentConfig['notify_url'],
                        "out_trade_no" => $payParam['trade_sn'],
                        "appid"        => $this->paymentConfig['app_id'],

                    ],
                    'headers' => ['Authorization' => $signStr, 'Accept' => 'application/json', 'Content-Type' => 'application/json']
                ]);
            $statusCode = $resp->getStatusCode();
            if ($statusCode != 200) {
                throw new ServiceException(ServiceExceptionConstant::getErrorData('wechat_unify_order_failed'));
            }
            return json_decode($resp->getBody()->getContents(), true);
        } catch (\Exception $e) {
            Log::error('下单失败：' . $e->getMessage());
            throw new ServiceException(ServiceExceptionConstant::getErrorData('下单失败'));
        }
    }

    //生成签名
    public function makeSign($nonceStr, $timeStamp, $prepayId = '')
    {
        $merchantPrivateKeyFilePath = 'file://' . dirname(__FILE__) . '/cert/apiclient_key.pem';
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);

        $params = [
            'appId'     => $this->paymentConfig['app_id'],
            'timeStamp' => (string)$timeStamp,
            'nonceStr'  => $nonceStr,
//            'package'   => 'prepay_id=' . $prepayId,
        ];
        if (!empty($prepayId)) {
            $params['package'] = 'prepay_id=' . $prepayId;
        }
        $params += ['paySign' => Rsa::sign(
            Formatter::joinedByLineFeed(...array_values($params)),
            $merchantPrivateKeyInstance
        ), 'signType'         => 'RSA'];
        return $params;
    }

    //回调验签
    public function checkNotifySign()
    {
        try {
            $headerData           = request()->header();
            $inWechatpaySignature = $headerData['wechatpay-signature'][0];// 请根据实际情况获取
            $inWechatpayTimestamp = $headerData['wechatpay-timestamp'][0];// 请根据实际情况获取
            $inWechatpaySerial    = $headerData['wechatpay-serial'][0];// 请根据实际情况获取
            $inWechatpayNonce     = $headerData['wechatpay-nonce'][0];// 请根据实际情况获取
            $inBody               = file_get_contents('php://input', 'r');// 请根据实际情况获取，例如: file_get_contents('php://input');

            $apiv3Key = $this->paymentConfig['key'];// 在商户平台上设置的APIv3密钥

            // 根据通知的平台证书序列号，查询本地平台证书文件，
            $platformPublicKeyInstance = Rsa::from('file://' . dirname(__FILE__) . '/cert/cert.pem', Rsa::KEY_TYPE_PUBLIC);

            // 检查通知时间偏移量，允许5分钟之内的偏移
            $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
            $verifiedStatus   = Rsa::verify(
            // 构造验签名串
                Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
                $inWechatpaySignature,
                $platformPublicKeyInstance
            );
            if ($timeOffsetStatus && $verifiedStatus) {
                // 转换通知的JSON文本消息为PHP Array数组
                $inBodyArray = (array)json_decode($inBody, true);
                // 使用PHP7的数据解构语法，从Array中解构并赋值变量
                ['resource' => [
                    'ciphertext'      => $ciphertext,
                    'nonce'           => $nonce,
                    'associated_data' => $aad
                ]] = $inBodyArray;
                // 加密文本消息解密
                $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
                // 把解密后的文本转换为PHP Array数组
                return (array)json_decode($inBodyResource, true);
            }
        } catch (\Exception $e) {
            Log::error('回调验证失败：' . $e->getMessage() . $e->getLine() . '，数据集：' . $inBody);
            return false;
        }
    }

    //发起退款申请
    public function refundOrder($order, $refund_order_sn)
    {
        $instance = $this->initHttpClient();
        try {
            $resp       = $instance
                ->chain('v3/refund/domestic/refunds')
                ->post([
                    'json'    => [
                        'out_trade_no'  => $order['order_sn'],
                        'out_refund_no' => $refund_order_sn,
                        'notify_url'    => $this->paymentConfig['refund_notify_url'],
                        'amount'        => [
                            'refund'   => $this->paymentConfig['pay_test'] ? 1 : $order['pay_total'],
                            'total'    => $this->paymentConfig['pay_test'] ? 1 : $order['pay_total'],
                            'currency' => 'CNY'
                        ]

                    ],
                    'headers' => ['Accept' => 'application/json']
                ]);
            $statusCode = $resp->getStatusCode();
            if ($statusCode != 200) {
                throw new ServiceException(ServiceExceptionConstant::getErrorData('wechat_unify_order_failed'));
            }
            $rel = json_decode($resp->getBody()->getContents(), true);
            Log::info('创建微信退款订单返回参数', $rel);
        } catch (\Exception $e) {
            Log::error('创建微信退款订单失败：' . $e->getMessage());
            throw new ServiceException(ServiceExceptionConstant::getErrorData('申请退款失败'));
        }
        return true;
    }
}
